/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 1997-2007 Sun Microsystems, Inc.  Portions Copyrighted 2008 Richard Schilling.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License. You can obtain
 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 *
 * Contributor(s): Richard Schilling (coderroadie@gmail.com)
 *
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.mail.iap;

import java.util.Vector;
import java.io.*;
import com.sun.mail.util.*;

/**
 * @author John Mani
 */

public class Argument {

    protected Vector items;

    /**
     * Constructor
     */
    public Argument() {
        items = new Vector(1);
    }

    /**
     * append the given Argument to this Argument. All items
     * from the source argument are copied into this destination
     * argument.
     */
    public void append(Argument arg) {
        items.ensureCapacity(items.size() + arg.items.size());
        for (int i = 0; i < arg.items.size(); i++)
            items.addElement(arg.items.elementAt(i));
    }

    /**
     * Write out given string as an ASTRING, depending on the type
     * of the characters inside the string. The string should
     * contain only ASCII characters.
     * <p>
     * XXX: Hmm .. this should really be called writeASCII()
     * 
     * @param s String to write out
     */
    public void writeString(String s) {
        items.addElement(new AString(ASCIIUtility.getBytes(s)));
    }

    /**
     * Convert the given string into bytes in the specified
     * charset, and write the bytes out as an ASTRING
     */
    public void writeString(String s, String charset) throws UnsupportedEncodingException {
        if (charset == null) // convenience
            writeString(s);
        else
            items.addElement(new AString(s.getBytes(charset)));
    }

    /**
     * Write out given byte[] as a Literal.
     * 
     * @param b byte[] to write out
     */
    public void writeBytes(byte[] b) {
        items.addElement(b);
    }

    /**
     * Write out given ByteArrayOutputStream as a Literal.
     * 
     * @param b ByteArrayOutputStream to be written out.
     */
    public void writeBytes(ByteArrayOutputStream b) {
        items.addElement(b);
    }

    /**
     * Write out given data as a literal.
     * 
     * @param b Literal representing data to be written out.
     */
    public void writeBytes(Literal b) {
        items.addElement(b);
    }

    /**
     * Write out given string as an Atom. Note that an Atom can contain only
     * certain US-ASCII characters. No validation is done on the characters
     * in the string.
     * 
     * @param s String
     */
    public void writeAtom(String s) {
        items.addElement(new Atom(s));
    }

    /**
     * Write out number.
     * 
     * @param i number
     */
    public void writeNumber(int i) {
        items.addElement(new Integer(i));
    }

    /**
     * Write out number.
     * 
     * @param i number
     */
    public void writeNumber(long i) {
        items.addElement(new Long(i));
    }

    /**
     * Write out as parenthesised list.
     * 
     * @param s statement
     */
    public void writeArgument(Argument c) {
        items.addElement(c);
    }

    /*
     * Write out all the buffered items into the output stream.
     */
    public void write(Protocol protocol) throws IOException, ProtocolException {
        int size = items != null ? items.size() : 0;
        DataOutputStream os = (DataOutputStream) protocol.getOutputStream();

        for (int i = 0; i < size; i++) {
            if (i > 0)	// write delimiter if not the first item
                os.write(' ');

            Object o = items.elementAt(i);
            if (o instanceof Atom) {
                os.writeBytes(((Atom) o).string);
            } else if (o instanceof Number) {
                os.writeBytes(((Number) o).toString());
            } else if (o instanceof AString) {
                astring(((AString) o).bytes, protocol);
            } else if (o instanceof byte[]) {
                literal((byte[]) o, protocol);
            } else if (o instanceof ByteArrayOutputStream) {
                literal((ByteArrayOutputStream) o, protocol);
            } else if (o instanceof Literal) {
                literal((Literal) o, protocol);
            } else if (o instanceof Argument) {
                os.write('('); // open parans
                ((Argument) o).write(protocol);
                os.write(')'); // close parans
            }
        }
    }

    /**
     * Write out given String as either an Atom, QuotedString or Literal
     */
    private void astring(byte[] bytes, Protocol protocol) throws IOException, ProtocolException {
        DataOutputStream os = (DataOutputStream) protocol.getOutputStream();
        int len = bytes.length;

        // If length is greater than 1024 bytes, send as literal
        if (len > 1024) {
            literal(bytes, protocol);
            return;
        }

        // if 0 length, send as quoted-string
        boolean quote = len == 0 ? true : false;
        boolean escape = false;

        byte b;
        for (int i = 0; i < len; i++) {
            b = bytes[i];
            if (b == '\0' || b == '\r' || b == '\n' || ((b & 0xff) > 0177)) {
                // NUL, CR or LF means the bytes need to be sent as literals
                literal(bytes, protocol);
                return;
            }
            if (b == '*' || b == '%' || b == '(' || b == ')' || b == '{' || b == '"' || b == '\\'
                    || ((b & 0xff) <= ' ')) {
                quote = true;
                if (b == '"' || b == '\\') // need to escape these characters
                    escape = true;
            }
        }

        if (quote) // start quote
            os.write('"');

        if (escape) {
            // already quoted
            for (int i = 0; i < len; i++) {
                b = bytes[i];
                if (b == '"' || b == '\\')
                    os.write('\\');
                os.write(b);
            }
        } else
            os.write(bytes);

        if (quote) // end quote
            os.write('"');
    }

    /**
     * Write out given byte[] as a literal
     */
    private void literal(byte[] b, Protocol protocol) throws IOException, ProtocolException {
        startLiteral(protocol, b.length).write(b);
    }

    /**
     * Write out given ByteArrayOutputStream as a literal.
     */
    private void literal(ByteArrayOutputStream b, Protocol protocol) throws IOException,
            ProtocolException {
        b.writeTo(startLiteral(protocol, b.size()));
    }

    /**
     * Write out given Literal as a literal.
     */
    private void literal(Literal b, Protocol protocol) throws IOException, ProtocolException {
        b.writeTo(startLiteral(protocol, b.size()));
    }

    private OutputStream startLiteral(Protocol protocol, int size) throws IOException,
            ProtocolException {
        DataOutputStream os = (DataOutputStream) protocol.getOutputStream();
        boolean nonSync = protocol.supportsNonSyncLiterals();

        os.write('{');
        os.writeBytes(Integer.toString(size));
        if (nonSync) // server supports non-sync literals
            os.writeBytes("+}\r\n");
        else
            os.writeBytes("}\r\n");
        os.flush();

        // If we are using synchronized literals, wait for the server's
        // continuation signal
        if (!nonSync) {
            for (;;) {
                Response r = protocol.readResponse();
                if (r.isContinuation())
                    break;
                if (r.isTagged())
                    throw new LiteralException(r);
                // XXX - throw away untagged responses;
                // violates IMAP spec, hope no servers do this
            }
        }
        return os;
    }
}

class Atom {

    String string;

    Atom(String s) {
        string = s;
    }
}

class AString {

    byte[] bytes;

    AString(byte[] b) {
        bytes = b;
    }
}
